大家好,我是韋恩,今天是第十二天,今天我們會介紹DataProvider的原理與相關觀念,並實際練習新增、修改與刪除TreeDataProvider的樹狀選單上的選項。
在剛才的範例裡,我們已經成功了register樹狀選單的Data Provider,接下來,我們需要能夠新增選單的選項,並能夠通知VSCode刷新介面。為此,我們需要VSCode的事件發射器EventEmitter。
那什麼是EventEmitter呢?簡單來說,EventEmitter是一個實做了觀察者模式的物件。在VSCode裡,vscode.EventEmitter
提供了一個fire方法做事件發射與通知,同時提供一個event方法讓想接收事件的人訂閱、監聽事件的消息。
vscode.EventEmitter
的用法如下:
/**
* 創建新的EventEmitter物件
*/
const eventEmitter = new vscode.EventEmitter();
/**
* 發射事件相關資料
*/
eventEmitter.fire('new changed data!');
/**
* 監聽與事件相關資料
*/
eventEmitter.event(message => {
console.log(`Receive message: ${message}`); // 'Receive message: new changed data!'
});
透過上例,我們可以看到EventEmitter提供了兩個方法,fire(發送事件源資料)
與event(監聽、接收事件源資料)
。這是為什麼這樣區分呢?
藉由emitter物件的方法,我們可以徹底的將
事件發生的源頭
與事件對應的處理方式
切分開來。
在VSCode裡,複雜的元件,透過TreeDataProvider
提供event的方法監聽資料改變的事件,因此我們的DataProvider會將事件監聽的方法暴露出去,如下所示:
class DataProvider implements vscode.TreeDataProvider<TreeViewItem> {
private eventEmitter = new vscode.EventEmitter<TreeViewItem | undefined | void>();
public get onDidChangeTreeData(): vscode.Event<TreeViewItem | undefined | void> {
return this.eventEmitter.event;
}
...
}
我們可以看到,onDidChangeTreeData
只會回傳eventEmitter.event
,而不會像上上面的用法直接收聽event,這樣的用法保證了DataProvider可以被外部監聽事件。所以這個暴露出去onDidChangeTreeData
方法,其實就如同我們昨天提到的創建類元件的監聽用法。
在昨天我們提到,創建類元件是可以被收聽資料改變的事件的。當我們create一個元件,我們可以使用元件裡的監聽方法,如createInputBox
。
const inputBox = vscode.window.createInputBox();
inputBox.onDidChangeValue((message: string) => {
...
})
onDidChangeValue
可以被外部監聽多次,也可以被多個對象監聽。
現在我們提供的onDidChangeTreeData
,沒有什麼不同,它一樣可以保證DataProvider可以被外部監聽事件,監聽多次,也可以被多個對象監聽,只是這次我們改為讓VSCode內部自己決定怎麼使用這個監聽方法,如下所示:
const dataProvider = new DataProvider();
dataProvider.onDidChangeTreeData((data: TreeViewItem | undefined | void) => {
const treeItemsData = dataProvider.getChildren();
...
});
從上面的範例我們很容易了解,DataProvider只會負責提供onDidChangeTreeData監聽事件,被註冊後,VSCode會自己監聽TreeData的資料,並且決定要怎麼處理資料與刷新VSCode視圖的UI元件,無需開發者費神。
與之對應的,dataProvider也應該提供事件通知的方法讓外部可以傳送新的資料,並讓VSCode接收到,這就是我們開發者要手動操作的方法了,底下我們會透過實作新增與刪除資料的實例來練習這部分。
註:跟Nodejs內建的EventEmitter或其他程式框架常見的EventBus比起來,
vscode.EventEmitter
的用法簡單與簡化許多。通常在其他library的實作裡,一個eventEmitter可以監聽多個事件,同時在傳送事件源頭的資料時指定其中一個事件發射資料,但在vscode.EventEmitter
裡,emitter並不會監聽多個事件,而僅會將不同事件源頭的資料發佈給訂閱者,這是專為VSCode的元件特化的設計。
好的,我們又了解了不少觀念與知識呢!讓我們繼續回到實作吧! 練習,將理論落地,是一把真正掌握一門知識與技藝的鑰匙。讓我們開始為使用者提供新增TreeView資料與刪除資料的功能吧!
yo code
Contribution Points
命令配置:讓我們照以下指示設定extension專案,在packagage.json,我們要新增一個ViewContainer,用於註冊VSCode左邊的activitybar,並指定一個新的explorer。之後,我們在Contribution Points
裡的View裡使用剛註冊完的explorer-id底下註冊TreeView跟相關Command,如下所示:
{
...
"contributes": {
"viewsContainers": {
"activitybar": [
{
"id": "treeview-explorer",
"title": "TreeView Explorer",
"icon": "assets/output.svg"
}
]
},
"views": {
"treeview-explorer": [
{
"id": "treeview",
"name": "TreeView Explorer",
"icon": "assets/output.svg",
"contextualTitle": "TreeView Explorer"
}
]
},
"viewsWelcome": [
{
"view": "treeview",
"contents": "Welcome to NewTreeView \n [learn more](https://code.visualstudio.com/api/extension-guides/tree-view/).\n[Resgister Data Provider](command:day13-dataprovider.registerDataProvider)",
}
],
...
},
...
}
註冊完上面配置後可以在VSCode的左側看到左下的activityBar已經註冊了TreeView Explorer,點擊可以展開我們自訂的Explorer。
{
...
"contributes": {
...
"commands": [
{
"command": "day13-dataprovider.registerDataProvider",
"title": "Day13: Register DataProvider"
},
{
"command": "day13-dataprovider.01_add",
"title": "Day13: Add New Item"
},
{
"command": "day13-dataprovider.02_edit",
"title": "Day13: Edit Exist Item",
"icon": {
"light": "assets/edit.svg",
"dark": "assets/edit.svg"
}
},
{
"command": "day13-dataprovider.03_delete",
"title": "Day13: Delete Item",
"icon": {
"light": "assets/trash.svg",
"dark": "assets/trash.svg"
}
}
]
...
},
...
}
{
...
"contributes": {
...
"menus": {
"view/title": [
{
"command": "day13-dataprovider.01_add",
"when": "view == treeview"
}
],
"view/item/context": [
{
"command": "day13-dataprovider.02_edit",
"when": "view == treeview && viewItem == treeviewitem",
"group": "inline"
},
{
"command": "day13-dataprovider.03_delete",
"when": "view == treeview && viewItem == treeviewitem",
"group": "inline"
},
{
"command": "day13-dataprovider.03_delete",
"when": "view == treeview && viewItem == treeviewitem"
}
]
}
...
},
...
}
treeview-data-provider.ts
)
import * as vscode from 'vscode';
export class TreeViewItem extends vscode.TreeItem {
constructor(label: string, collapsibleState?: vscode.TreeItemCollapsibleState) {
super(label, collapsibleState);
this.contextValue = 'treeviewitem';
}
}
export class DataProvider implements vscode.TreeDataProvider<TreeViewItem> {
private dataStorage = [
new TreeViewItem('TreeItem-01'),
new TreeViewItem('TreeItem-02'),
new TreeViewItem('TreeItem-03'),
];
private eventEmitter = new vscode.EventEmitter<TreeViewItem | undefined | void>();
public get onDidChangeTreeData(): vscode.Event<TreeViewItem | undefined | void> {
return this.eventEmitter.event;
}
public getTreeItem(element: TreeViewItem): vscode.TreeItem | Thenable<vscode.TreeItem> {
return element;
}
public getChildren(element?: TreeViewItem): vscode.ProviderResult<TreeViewItem[]> {
return Promise.resolve(this.dataStorage);
}
public addItem(newItem: TreeViewItem) {
this.dataStorage.push(newItem);
this.updateView();
}
public editItem(item: TreeViewItem, name: string) {
const editItem = this.dataStorage.find(i => i.label === item.label);
if (editItem) {
editItem.label = name;
this.updateView();
}
}
public deleteItem(item: TreeViewItem) {
this.dataStorage = this.dataStorage.filter(i => i.label !== item.label);
this.updateView();
}
private updateView() {
this.eventEmitter.fire();
}
}
extension.ts
dataProvider與命令邏輯設計import * as vscode from 'vscode';
import { DataProvider, TreeViewItem } from './treeview-data-provider';
export function activate(context: vscode.ExtensionContext) {
const dataProvider = new DataProvider();
let initView = vscode.commands.registerCommand('day13-dataprovider.registerDataProvider', () => {
vscode.window.registerTreeDataProvider('treeview', dataProvider);
vscode.window.showInformationMessage('Create day13-treeview!');
});
let addItem = vscode.commands.registerCommand('day13-dataprovider.01_add', async () => {
const itemId = await vscode.window.showInputBox({
placeHolder: 'Your New TreeItem Id'
}) || '';
if (itemId !== '') {
dataProvider.addItem(new TreeViewItem(itemId));
}
vscode.window.showInformationMessage('Add Day-13 TreeViewItem!');
});
let editItem = vscode.commands.registerCommand('day13-dataprovider.02_edit', async (item: TreeViewItem) => {
const itemName = await vscode.window.showInputBox({
placeHolder: 'Your New TreeItem Name'
}) || '';
if (itemName !== '') {
dataProvider.editItem(item, itemName);
}
vscode.window.showInformationMessage('Edit Day-13 TreeViewItem!');
});
let deleteItem = vscode.commands.registerCommand('day13-dataprovider.03_delete', async (item: TreeViewItem) => {
const confirm = await vscode.window.showQuickPick(['delete', 'canel'], {
placeHolder: 'Do you want to delete item?'
});
if (confirm === 'delete') {
dataProvider.deleteItem(item);
}
vscode.window.showInformationMessage('Delete Day-13 TreeViewItem!');
});
context.subscriptions.push(initView, addItem, editItem, deleteItem);
}
export function deactivate() {}
好啦,今天,我們創建了解了TreeView的DataProvider設計方式,並了解TreeView的新增、刪除等使用方法。
明天會繼續VSCode元件的介紹與實際練習,我們明天見,謝謝大家。